from khronos.utils import Deque

##class TransitionDistr(object):
##    def __init__(self, start_distr, end_distr, start, end, degree):
##        self.start_distr = start_distr
##        self.end_distr = end_distr
##        self.start = start
##        self.end = end
##        self.degree = degree
##        
##    def __call__(self, time):
##        if not self.start <= time <= self.end:
##            raise ValueError("given time does not belong to the transition interval")
##        value0 = self.start_distr(time)
##        value1 = self.end_distr(time)
##        weight0 = ((time - self.start) / (self.end - self.start)) ** self.degree
##        weight1 = 1.0 - weight0
##        return value0 * weight0 + value1 * weight1
        
class TimeVaryingDistr(object):
    """This class allows definition of functions which assume the behavior of different 
    distributions based on time. For instance, this enables the definition of demand peaks 
    at certain hours of the day, and low demand during the remaining hours.
    NOTE: This class allows nested distribution objects, as they emulate regular Python functions.
    
    The following example demonstrates the use of this class to specify a possible distribution of 
    customer interarrival times at a shop during an entire week, with varying arrival rates. We 
    use three different rates: high (40 per hour), medium (25 per hour), and low (10 per hour). 
    We use these three distributions as the base to build the distribution of client rates on a 
    normal day and a bad day. These two time-varying distributions are finally used to create the 
    distribution for a normal week, where the first and last days are bad, and the remaining days 
    are normal.
    
    from random import Random
    
    rng  = Random()
    HOUR = 1.0
    DAY  = 24.0 * HOUR
    WEEK = 7.0 * DAY
    
    def high():   return rng.expovariate(40) * HOUR
    def medium(): return rng.expovariate(25) * HOUR
    def low():    return rng.expovariate(10) * HOUR
    
    normal_day = TimeVaryingDistr(name="normal_day", default=low, loop=DAY)
    normal_day.add(high, (8*HOUR, 10*HOUR), (18*HOUR, 20*HOUR))
    normal_day.add(medium, (7*HOUR, 8*HOUR), (10*HOUR, 11*HOUR), 
                           (17*HOUR, 18*HOUR), (20*HOUR, 21*HOUR))
    
    bad_day = TimeVaryingDistr(name="bad_day", default=low, loop=DAY)
    bad_day.add(high, (8*HOUR, 9*HOUR), (18*HOUR, 19*HOUR))
    bad_day.add(medium, (7.5*HOUR, 8*HOUR), (9*HOUR, 9.5*HOUR),
                        (17.5*HOUR, 18*HOUR), (19*HOUR, 19.5*HOUR))
    
    week = TimeVaryingDistr(name="normal_week", default=normal_day, loop=WEEK)
    week.add(bad_day, (0*DAY, 1*DAY), (6*DAY, 7*DAY))
    """
    def __init__(self, name="noname", default=None, loop=None, 
                 clock=None, transition_order=2, distrs=()):
        self.__distrs = Deque()
        self.__default = default
        self.__loop = loop
        self.__clock = clock
        self.__transorder = transition_order
        self.__name__ = name
        for distr, intervals in distrs:
            self.add(distr, *intervals)
            
    def __repr__(self):
        line_fmt = "%40s: %s"
        lines = [object.__repr__(self), "{"]
        lines.append(line_fmt % ("Clock", self.__clock))
        lines.append(line_fmt % ("Loop time", self.__loop))
        lines.append(line_fmt % ("Transition function order", self.__transorder))
        if self.__default is not None:
            lines.append(line_fmt % ("Default distribution", self.__default.__name__))
        for start, end, subdistr in self.__distrs:
            lines.append(line_fmt % ("[%s - %s[" % (start, end), subdistr.__name__))
        lines.append("}")
        return "\n".join(lines)
        
    def add(self, distr, *intervals):
        """Add a sub-distribution and its associated time intervals. Later, if the distribution 
        object is called using a time value that lies in any of these intervals, the corresponding
        sub-distribution is used to generate the result."""
        for start, end in intervals:
            self.__distrs.insert_sorted((start, end, distr))
            
    def set_default(self, default):
        """Define the default sub-distribution to be used when no other sub-distribution is 
        applicable to a given time value."""
        self.__default = default
        
    def set_loop(self, loop):
        """Set the distribution's loop time. The loop time makes the distribution exhibit the same 
        behavior periodically. For instance, if a distribution object has a loop time of 10.0, it 
        means that the same behavior is obtained at 12.0 and 22.0 because 12.0 % 10.0 is equal to 
        22.0 % 10."""
        self.__loop = loop
        
    def set_clock(self, clock):
        """Set the distribution's clock function (no arguments, returning a time)."""
        self.__clock = clock
        
    def set_transition_order(self, transition_order):
        """Set the exponent of the transition function used to compute the distribution's value 
        when multiple subdistributions apply. A transition order of 1 represents a linear 
        transition, 2 results in a quadratic transition function, etc."""
        self.__transorder = transition_order
        
    def __call__(self, time=None):
        """Get a sample from the time-varying distribution. Time can be given as argument or 
        obtained automatically from the distribution's clock function."""
        # If no time was provided, get the current time from the clock function. 
        # Afterwards, apply the loop time if it is defined. 
        if time is None:
            time = self.__clock()
        if self.__loop is not None:
            time %= self.__loop
        # Find intervals where the current time falls into.
        values = []
        for start, end, subdistr in self.__distrs:
            if start > time:
                break
            if time < end:
                if isinstance(subdistr, TimeVaryingDistr):
                    # If the interval contains a nested distribution object, pass it the time 
                    # elapsed since the beginning of its interval, not the absolute time.
                    values.append((start, end, subdistr(time - start)))
                else:
                    values.append((start, end, subdistr()))
        # Use the default distribution if the current time does not fall into any interval.
        if len(values) == 0:
            if self.__default is not None:
                if isinstance(self.__default, TimeVaryingDistr):
                    return self.__default(time)
                else:
                    return self.__default()
            raise ValueError("Unable to generate a value. Current time does not fall into "
                             "any interval, and no default distribution was specified.")
        # Only one distribution applies to this interval, no aggregation required.
        if len(values) == 1:
            return values[0][2]
        # Two overlapping intervals - aggregate values.
        if len(values) == 2:
            return self.__aggregate(values, time)
        raise Exception("More than two overlapping intervals. Cannot generate a value.")
        
    def __aggregate(self, values, time):
        weight = [0.5, 0.5]
        start_0, end_0, _ = values[0]
        start_1, end_1, _ = values[1]
        # Intersection of interval x and y
        start_i, end_i = max(start_0, start_1), min(end_0, end_1)
        amplitude_i = end_i - start_i
        midpoint_i = (start_i + end_i) / 2.0
        if time < midpoint_i and start_0 != start_1:
            weight_2nd = 0.5 * ((time - start_i) / (amplitude_i / 2.0)) ** self.__transorder
            weight_1st = 1.0 - weight_2nd
            start_order = (0, 1) if start_0 < start_1 else (1, 0)
            weight[start_order[0]] = weight_1st
            weight[start_order[1]] = weight_2nd
        elif time > midpoint_i and end_0 != end_1:
            weight_1st = 0.5 * ((end_i - time) / (amplitude_i / 2.0)) ** self.__transorder
            weight_2nd = 1.0 - weight_1st
            end_order = (0, 1) if end_0 < end_1 else (1, 0)
            weight[end_order[0]] = weight_1st
            weight[end_order[1]] = weight_2nd
        return values[0][2] * weight[0] + values[1][2] * weight[1]
        